package com.roamcattery.plugins.load;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.AbstractTemplateEngine;
import com.roamcattery.plugins.constant.PropertyConstant;
import com.roamcattery.plugins.entity.CustomizeConfig;
import com.roamcattery.plugins.entity.Generator;
import com.roamcattery.plugins.entity.OtherFile;
import com.roamcattery.plugins.util.CollectionUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;


import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;


/**
 * @author wangwc@wumart.com
 * @version v1.0.0
 * @date 2022-6-30 18:23
 */
@Slf4j
public class XmlPropertiesParses implements PropertiesLoadParser {


    private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static final Map<Class<?>,Invoker> CLAZZ_VALUEOF_INVOKER = new ConcurrentHashMap<>();

    @SneakyThrows
    @Override
    public Generator load(String resourcePath) {
        Generator generator = new Generator();
        //1.创建Reader对象
        SAXReader saxReader = new SAXReader();
        //2.加载xml
        Document document = saxReader.read(new File(resourcePath));
        //3.获取根节点
        Element rootElement = document.getRootElement();
        // rootElement.elements()获取根节点下所有的节点，
        //List<Element> elements = rootElement.elements();

        Element datasourceElement = rootElement.element(PropertyConstant.dataSource_config);
        Element globalConfigElement = rootElement.element(PropertyConstant.global_config);
        Element packageConfigElement = rootElement.element(PropertyConstant.package_config);
        Element strategyConfigElement = rootElement.element(PropertyConstant.strategy_config);
        Element customizeConfigElement = rootElement.element(PropertyConstant.customize_config);

      /*  for (Element element : elements) {
            //依次遍历根节点下的节点  book book book1
            element.getName();
            //依次遍历根节点下的节点对应属性为id的值 001 002 003
            System.out.println(element.attribute("id").getValue());
            System.out.println("----------------------------");
            //同样道理 继续依次获取该节点下的节点
            List<Element> child = element.elements();
            for (Element element1 : child) {
                //依次/获取子节点  id title author   、title  author、title author
                element1.getName();
                //依次获取子节点的值 9  Harry Potter  J K. Rowling、.......
                System.out.println(element1.getText());
            }
        }
        // 获取第二个节点book的属性为id的值 002
        elements.get(1).attribute("id").getValue();
        // 获取根节点下第二个节点book所有节点
        List<Element> elements1 = elements.get(1).elements();
        //获取根节点下第二个节点book节点下的第二个节点author的值 Erik T. Ray
        elements.get(1).elements().get(1).getText();*/

        DataSourceConfig dataSourceConfig = this.parseDataSourceConfig(datasourceElement);
        GlobalConfig globalConfig = this.parseGlobalConfig(globalConfigElement);
        PackageConfig packageConfig = this.parsePackageConfig(packageConfigElement);
        StrategyConfig strategyConfig = this.parseStrategyConfig(strategyConfigElement);
        CustomizeConfig customizeConfig = this.parseCustomizeConfig(customizeConfigElement);
        generator.setDataSourceConfig(dataSourceConfig);
        generator.setGlobalConfig(globalConfig);
        generator.setPackageConfig(packageConfig);
        generator.setStrategyConfig(strategyConfig);
        generator.setCustomizeConfig(customizeConfig);
        return generator;
    }


    private DataSourceConfig parseDataSourceConfig(Element element) {
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        Map<String, String> propertiesMap = this.parseProperties(element);
        List<String> dataSourceConfigEnumProperties = getEnumProperties(PropertyConstant.DataSourceConfigProperties.class);
        Map<String, Object> dataSourceConfigEnumMap = propertiesMap.entrySet().stream().filter(e -> dataSourceConfigEnumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        String url = this.getElementValue(element, PropertyConstant.url);
        if (url == null) {
            url = this.stringValueOf(dataSourceConfigEnumMap.get(PropertyConstant.url));
        }
        log.info("url:{}", url);
        String driverName = this.getElementValue(element, PropertyConstant.driver_name);
        if (driverName == null) {
            driverName = this.stringValueOf(dataSourceConfigEnumMap.get(PropertyConstant.driver_name));
        }
        log.info("driverName:{}", driverName);
        String userName = this.getElementValue(element, PropertyConstant.username);
        if (userName == null) {
            userName = this.stringValueOf(dataSourceConfigEnumMap.get(PropertyConstant.username));
        }
        log.info("userName:{}", userName);
        String password = this.getElementValue(element, PropertyConstant.password);
        if (password == null) {
            password = this.stringValueOf(dataSourceConfigEnumMap.get(PropertyConstant.password));
        }
        log.info("password:{}", password);
        Map<String, String> reverseMap = propertiesMap.entrySet().stream().filter(e -> !dataSourceConfigEnumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        dataSourceConfig = propertiesReflect(dataSourceConfig,reverseMap);

        dataSourceConfig.setUrl(url);
        dataSourceConfig.setDriverName(driverName);
        dataSourceConfig.setUsername(userName);
        dataSourceConfig.setPassword(password);
        return dataSourceConfig;
    }

    public <T> T propertiesReflect(T obj,Map<String, String> reverseMap){

        Set<String> propertiesNames = reverseMap.keySet();
        Field[] fields = obj.getClass().getDeclaredFields();

        Stream.of(fields).filter(field->propertiesNames.contains(field.getName()))
                .forEach(field -> {
                    try {
                        field.setAccessible(true);
                        String value = reverseMap.get(field.getName());
                        //this.parseValue(field)
                        Object parseValue = this.parseValue(field.getDeclaringClass(),value);
                        field.set(obj, parseValue);
                    }catch (Exception e){
                        log.error("反射装填错误",e);
                    }
                });
        return obj;
    }

    private GlobalConfig parseGlobalConfig(Element element) {

        Map<String, String> propertiesMap = this.parseProperties(element);
        List<String> enumProperties = getEnumProperties(PropertyConstant.GlobalConfigProperties.class);
        Map<String, Object> configEnumMap = propertiesMap.entrySet().stream().filter(e -> enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        String outputDir = this.getElementValue(element, PropertyConstant.output_dir);
        if (outputDir == null) {
            outputDir = this.stringValueOf(configEnumMap.get(PropertyConstant.output_dir));
        }
        log.info("outputDir:{}",outputDir);
        String author = this.getElementValue(element, PropertyConstant.author);
        if (author == null) {
            author = this.stringValueOf(configEnumMap.get(PropertyConstant.author));
        }
        log.info("author:{}",author);
        Boolean open = this.getElementBooleanValue(element, PropertyConstant.open);
        if (open == null) {
            open = this.booleanValueOf(configEnumMap.get(PropertyConstant.open));
        }
        log.info("open:{}",open);
        Boolean fileOverride = this.getElementBooleanValue(element, PropertyConstant.file_override);
        if (fileOverride == null) {
            fileOverride = this.booleanValueOf(configEnumMap.get(PropertyConstant.file_override));
        }
        log.info("fileOverride:{}",fileOverride);
        Boolean enableCache = this.getElementBooleanValue(element, PropertyConstant.enable_cache);
        if (enableCache == null) {
            enableCache = this.booleanValueOf(configEnumMap.get(PropertyConstant.enable_cache));
        }
        log.info("enableCache:{}",enableCache);
        String idType = this.getElementValue(element, PropertyConstant.id_type);
        if (idType == null) {
            idType = this.stringValueOf(configEnumMap.get(PropertyConstant.id_type));
        }
        log.info("idType:{}",idType);
        String dateType = this.getElementValue(element, PropertyConstant.date_type);
        if (dateType == null) {
            dateType = this.stringValueOf(configEnumMap.get(PropertyConstant.date_type));
        }
        log.info("dateType:{}",dateType);
        Boolean baseColumnList = this.getElementBooleanValue(element, PropertyConstant.base_column_list);
        if (baseColumnList == null) {
            baseColumnList = this.booleanValueOf(configEnumMap.get(PropertyConstant.base_column_list));
        }
        log.info("baseColumnList:{}",baseColumnList);
        Boolean baseResultMap = this.getElementBooleanValue(element, PropertyConstant.base_result_map);
        if (baseResultMap == null) {
            baseResultMap = this.booleanValueOf(configEnumMap.get(PropertyConstant.base_result_map));
        }
        log.info("baseResultMap:{}",baseResultMap);
        Boolean swagger2 = this.getElementBooleanValue(element, PropertyConstant.swagger2);
        if (swagger2 == null) {
            swagger2 = this.booleanValueOf(configEnumMap.get(PropertyConstant.swagger2));
        }
        log.info("swagger2:{}",swagger2);
        GlobalConfig globalConfig = new GlobalConfig();

        Map<String, String> reverseMap = propertiesMap.entrySet().stream().filter(e -> !enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        globalConfig = propertiesReflect(globalConfig,reverseMap);

        globalConfig.setOutputDir(outputDir);
        globalConfig.setAuthor(author);
        globalConfig.setOpen(open);
        globalConfig.setFileOverride(fileOverride);
        globalConfig.setEnableCache(enableCache);
        globalConfig.setIdType(IdType.valueOf(idType.toUpperCase()));
        globalConfig.setDateType(DateType.valueOf(dateType.toUpperCase()));
        globalConfig.setBaseColumnList(baseColumnList);
        globalConfig.setBaseResultMap(baseResultMap);
        globalConfig.setSwagger2(swagger2);
        return globalConfig;
    }

    private PackageConfig parsePackageConfig(Element element) {
        PackageConfig packageConfig = new PackageConfig();
        Map<String, String> propertiesMap = this.parseProperties(element);
        List<String> enumProperties = getEnumProperties(PropertyConstant.PackageConfigProperties.class);
        Map<String, Object> configEnumMap = propertiesMap.entrySet().stream().filter(e -> enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        String parent = this.getElementValue(element, PropertyConstant.parent);
        if (parent == null) {
            parent = this.stringValueOf(configEnumMap.get(PropertyConstant.parent));
        }
        log.info("parent:{}",parent);
        String moduleName = this.getElementValue(element, PropertyConstant.module_name);
        if (moduleName == null) {
            moduleName = this.stringValueOf(configEnumMap.get(PropertyConstant.module_name));
        }
        log.info("moduleName:{}",moduleName);
        String entity = this.getElementValue(element, PropertyConstant.entity);
        if (entity == null) {
            entity = this.stringValueOf(configEnumMap.get(PropertyConstant.entity));
        }
        log.info("entity:{}",entity);
        String mapper = this.getElementValue(element, PropertyConstant.mapper);
        if (mapper == null) {
            mapper = this.stringValueOf(configEnumMap.get(PropertyConstant.mapper));
        }
        log.info("mapper:{}",mapper);
        String service = this.getElementValue(element, PropertyConstant.service);
        if (service == null) {
            service = this.stringValueOf(configEnumMap.get(PropertyConstant.service));
        }
        log.info("service:{}",service);
        String serviceImpl = this.getElementValue(element, PropertyConstant.service_impl);
        if (serviceImpl == null) {
            serviceImpl = this.stringValueOf(configEnumMap.get(PropertyConstant.service_impl));
        }
        log.info("serviceImpl:{}",serviceImpl);
        String controller = this.getElementValue(element, PropertyConstant.controller);
        if (controller == null) {
            controller = this.stringValueOf(configEnumMap.get(PropertyConstant.controller));
        }
        log.info("controller:{}",controller);

        Map<String, String> reverseMap = propertiesMap.entrySet().stream().filter(e -> !enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        packageConfig = propertiesReflect(packageConfig,reverseMap);


        packageConfig.setParent(parent);
        packageConfig.setModuleName(moduleName);
        packageConfig.setEntity(entity);
        packageConfig.setMapper(mapper);
        packageConfig.setService(service);
        packageConfig.setServiceImpl(serviceImpl);
        packageConfig.setController(controller);
        return packageConfig;

    }

    private StrategyConfig parseStrategyConfig(Element element) {
        StrategyConfig strategyConfig = new StrategyConfig();
        Map<String, String> propertiesMap = this.parseProperties(element);
        List<String> enumProperties = getEnumProperties(PropertyConstant.StrategyConfigProperties.class);
        Map<String, Object> configEnumMap = propertiesMap.entrySet().stream().filter(e -> enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        String naming = this.getElementValue(element, PropertyConstant.naming);
        if (naming == null) {
            naming = this.stringValueOf(configEnumMap.get(PropertyConstant.naming));
        }
        log.info("naming:{}",naming);
        String columnNaming = this.getElementValue(element, PropertyConstant.column_naming);
        if (columnNaming == null) {
            columnNaming = this.stringValueOf(configEnumMap.get(PropertyConstant.column_naming));
        }
        log.info("columnNaming:{}",columnNaming);
        Boolean entityLombokModel = this.getElementBooleanValue(element, PropertyConstant.entity_lombok_model);
        if (entityLombokModel == null) {
            entityLombokModel = this.booleanValueOf(configEnumMap.get(PropertyConstant.entity_lombok_model));
        }
        log.info("entityLombokModel:{}",entityLombokModel);
        Boolean restControllerStyle = this.getElementBooleanValue(element, PropertyConstant.rest_controller_style);
        if (restControllerStyle == null) {
            restControllerStyle = this.booleanValueOf(configEnumMap.get(PropertyConstant.rest_controller_style));
        }
        log.info("restControllerStyle:{}",restControllerStyle);
        Boolean controllerMappingHyphenStyle = this.getElementBooleanValue(element, PropertyConstant.controller_mapping_hyphen_style);
        if (controllerMappingHyphenStyle == null) {
            controllerMappingHyphenStyle = this.booleanValueOf(configEnumMap.get(PropertyConstant.controller_mapping_hyphen_style));
        }
        log.info("controllerMappingHyphenStyle:{}",controllerMappingHyphenStyle);
        String tablePrefix = this.getElementValue(element, PropertyConstant.table_prefix);
        if (tablePrefix == null) {
            tablePrefix = this.stringValueOf(configEnumMap.get(PropertyConstant.table_prefix));
        }
        log.info("tablePrefix:{}",tablePrefix);
        Boolean entityBooleanColumnRemoveIsPrefix = this.getElementBooleanValue(element, PropertyConstant.entity_boolean_column_remove_is_prefix);
        if (entityBooleanColumnRemoveIsPrefix == null) {
            entityBooleanColumnRemoveIsPrefix = this.booleanValueOf(configEnumMap.get(PropertyConstant.entity_boolean_column_remove_is_prefix));
        }
        log.info("entityBooleanColumnRemoveIsPrefix:{}",entityBooleanColumnRemoveIsPrefix);
        Boolean entityColumnConstant = this.getElementBooleanValue(element, PropertyConstant.entity_column_constant);
        if (entityColumnConstant == null) {
            entityColumnConstant = this.booleanValueOf(configEnumMap.get(PropertyConstant.entity_column_constant));
        }
        log.info("entityColumnConstant:{}",entityColumnConstant);
        Element includeEle = element.element(PropertyConstant.include);
        List<Element> tableEle = includeEle.elements(PropertyConstant.table);
        String[] include = {};
        if (CollectionUtils.isNotEmpty(tableEle)){
            include = tableEle.stream().map(e->e.attributeValue(PropertyConstant.value)).toArray(String[]::new);
        }

        Map<String, String> reverseMap = propertiesMap.entrySet().stream().filter(e -> !enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        strategyConfig = propertiesReflect(strategyConfig,reverseMap);


        strategyConfig.setNaming(NamingStrategy.valueOf(naming));
        strategyConfig.setColumnNaming(NamingStrategy.valueOf(columnNaming));
        strategyConfig.setEntityLombokModel(entityLombokModel);
        strategyConfig.setRestControllerStyle(restControllerStyle);
        strategyConfig.setControllerMappingHyphenStyle(controllerMappingHyphenStyle);
        strategyConfig.setTablePrefix(tablePrefix);
        strategyConfig.setEntityBooleanColumnRemoveIsPrefix(entityBooleanColumnRemoveIsPrefix);
        strategyConfig.setEntityColumnConstant(entityColumnConstant);
        strategyConfig.setInclude(include);
        return strategyConfig;
    }

    private CustomizeConfig parseCustomizeConfig(Element element) {
        CustomizeConfig customizeConfig = new CustomizeConfig();
        Map<String, String> propertiesMap = this.parseProperties(element);
        List<String> enumProperties = getEnumProperties(PropertyConstant.StrategyConfigProperties.class);
        Map<String, Object> configEnumMap = propertiesMap.entrySet().stream().filter(e -> enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        String xmlFileOutPath = this.getElementValue(element, PropertyConstant.xml_file_out_path);
        if (xmlFileOutPath == null) {
            xmlFileOutPath = this.stringValueOf(configEnumMap.get(PropertyConstant.xml_file_out_path));
        }
        log.info("xmlFileOutPath:{}",xmlFileOutPath);

        String xmlTemplatePath = this.getElementValue(element, PropertyConstant.xml_template_path);
        if (xmlTemplatePath == null) {
            xmlTemplatePath = this.stringValueOf(configEnumMap.get(PropertyConstant.xml_template_path));
        }
        log.info("xmlTemplatePath:{}",xmlTemplatePath);
        Element fileMap = element.element(PropertyConstant.file_map);
        List<Element> files = fileMap.elements(PropertyConstant.file);
        List<OtherFile> otherFiles = files.stream().map(f->{
            String filePackage = f.attributeValue(PropertyConstant.file_package);
            String fileName = f.attributeValue( PropertyConstant.file_name);
            String fileTemplate = f.attributeValue( PropertyConstant.file_template);
            return new OtherFile(filePackage,fileName,fileTemplate);
        }).collect(Collectors.toList());
        log.info("fileMap:{}",otherFiles);
        Element paramEle = element.element(PropertyConstant.param);
        List<Element> paramProperty = paramEle.elements(PropertyConstant.property);
        Map<String,String> param = paramProperty.stream().collect(Collectors.toMap(e->e.attributeValue( PropertyConstant.name),e->e.attributeValue( PropertyConstant.value),(v1,v2)->v1));
        log.info("param:{}",param);


        Map<String, String> reverseMap = propertiesMap.entrySet().stream().filter(e -> !enumProperties.contains(e.getKey()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        customizeConfig = propertiesReflect(customizeConfig,reverseMap);

        customizeConfig.setXmlTemplatePath(xmlTemplatePath);
        customizeConfig.setXmlFileOutPath(xmlFileOutPath);
        customizeConfig.setFileMap(otherFiles);
        customizeConfig.setParam(param);
        return customizeConfig;
    }

    /**
     * 获取properties定义的属性
     *
     */
    public Map<String, String> parseProperties(Element element) {
        List<Element> propertiesElement = element.elements(PropertyConstant.property);
        return propertiesElement.stream()
                .collect(Collectors.toMap(e -> e.attributeValue(PropertyConstant.name), e -> e.attributeValue(PropertyConstant.value), (v1, v2) -> v1));
    }


    public static <E extends Enum<E>> List<String> getEnumProperties(Class<E> enumClass) {
        final Invoker invoker = getPropertiesInvoker(enumClass);
        E[] es = enumClass.getEnumConstants();
        List<String> properties = Arrays.stream(es).map((e) -> getValue(invoker, e)).collect(Collectors.toList());
        return properties;
    }

    public static <E extends Enum<E>> Invoker getPropertiesInvoker(Class<E> enumClass) {
        if (enumClass == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        MetaClass metaClass = MetaClass.forClass(enumClass, REFLECTOR_FACTORY);
        String name = PropertyConstant.property;
        return metaClass.getGetInvoker(name);
    }

    private static String getValue(Invoker invoker, Object object) {
        try {
            return String.valueOf(invoker.invoke(object, new Object[0]));
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }

    private String getElementValue(Element element, String name) {
        return element.element(name) == null ? null : element.element(name).attributeValue(PropertyConstant.value);
    }
    private Boolean getElementBooleanValue(Element element, String name) {
        return element.element(name) == null ? null : Boolean.valueOf(element.element(name).attributeValue(PropertyConstant.value));
    }

    private String stringValueOf(Object value) {
        return value == null ? null : String.valueOf(value);
    }

    private Boolean booleanValueOf(Object value) {
        return value == null ? null : Boolean.valueOf(value.toString());
    }

    private Integer integerValueOf(Object value) {
        return value == null ? null : Integer.valueOf(value.toString());
    }


    private Object parseValue(Invoker invoker, Object object) {
        try {
            return invoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }
    private Object parseValue(Class<?> clazz, Object object) {
        Method method = null;
        try {
            method = clazz.getDeclaredMethod("valueOf",String.class);
        } catch (NoSuchMethodException e) {
            log.warn("无valueOf转换方法,class:{},object:{}",clazz,object);
        }
        if (method == null){
            return object;
        }
        try {
            return method.invoke(null,object);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return object;
    }

    public static void main(String[] args) {
        XmlPropertiesParses xmlPropertiesParses = new XmlPropertiesParses();
        xmlPropertiesParses.load("");
    }

}
